/*
This source file is part of the open source TripleGasm project
Copyright (C) 2010 Roma Kovalev

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

For the latest info, see http://code.google.com/p/triplegasm
You can contact the author(s) via e-mail: endiannesz@gmail.com
*/

#include "basesphere.hpp"

using namespace std;
using namespace Ogre;
using namespace TripleGasm;

// Original credit for this function goes to mcspy0312 from a post of his on the
// Ogre3D forums, but it looks like he got it from elsewhere too.
// The code was modified and cleaned up to suit the BaseSphere class.
void BaseSphere::CreateSphere()
{
  MeshPtr pSphere = MeshManager::getSingleton().createManual("Sphere", ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
  SubMesh *pSphereVertex = pSphere->createSubMesh();

  pSphere->sharedVertexData = new VertexData();
  VertexData* vertexData = pSphere->sharedVertexData;

  // define the vertex format
  VertexDeclaration* vertexDecl = vertexData->vertexDeclaration;
  size_t currOffset = 0;
  // positions
  vertexDecl->addElement(0, currOffset, VET_FLOAT3, VES_POSITION);
  currOffset += VertexElement::getTypeSize(VET_FLOAT3);
  // normals
  vertexDecl->addElement(0, currOffset, VET_FLOAT3, VES_NORMAL);
  currOffset += VertexElement::getTypeSize(VET_FLOAT3);
  // two dimensional texture coordinates
  vertexDecl->addElement(0, currOffset, VET_FLOAT2, VES_TEXTURE_COORDINATES, 0);
  currOffset += VertexElement::getTypeSize(VET_FLOAT2);

  // allocate the vertex buffer
  vertexData->vertexCount = (SPHERE_RINGS + 1) * (SPHERE_SEGS + 1);
  HardwareVertexBufferSharedPtr vBuf = HardwareBufferManager::getSingleton().createVertexBuffer(vertexDecl->getVertexSize(0), vertexData->vertexCount, HardwareBuffer::HBU_STATIC_WRITE_ONLY, false);
  VertexBufferBinding* binding = vertexData->vertexBufferBinding;
  binding->setBinding(0, vBuf);
  float* pVertex = static_cast<float*>(vBuf->lock(HardwareBuffer::HBL_DISCARD));

  // allocate index buffer
  pSphereVertex->indexData->indexCount = 6 * SPHERE_RINGS * (SPHERE_SEGS + 1);
  pSphereVertex->indexData->indexBuffer = HardwareBufferManager::getSingleton().createIndexBuffer(HardwareIndexBuffer::IT_16BIT, pSphereVertex->indexData->indexCount, HardwareBuffer::HBU_STATIC_WRITE_ONLY, false);
  HardwareIndexBufferSharedPtr iBuf = pSphereVertex->indexData->indexBuffer;
  unsigned short* pIndices = static_cast<unsigned short*>(iBuf->lock(HardwareBuffer::HBL_DISCARD));

  float fDeltaRingAngle = (Math::PI / SPHERE_RINGS);
  float fDeltaSegAngle = (2 * Math::PI / SPHERE_SEGS);
  unsigned short wVerticeIndex = 0 ;

  // Generate the group of rings for the sphere
  for( int ring = 0; ring <= SPHERE_RINGS; ring++ ) {
    float r0 = SPHERE_RADIUS * sinf (ring * fDeltaRingAngle);
    float y0 = SPHERE_RADIUS * cosf (ring * fDeltaRingAngle);

    // Generate the group of segments for the current ring
    for(int seg = 0; seg <= SPHERE_SEGS; seg++) {
      float x0 = r0 * sinf(seg * fDeltaSegAngle);
      float z0 = r0 * cosf(seg * fDeltaSegAngle);

      // Add one vertex to the strip which makes up the sphere
      *pVertex++ = x0;
      *pVertex++ = y0;
      *pVertex++ = z0;

      Vector3 vNormal = Vector3(x0, y0, z0).normalisedCopy();
      *pVertex++ = vNormal.x;
      *pVertex++ = vNormal.y;
      *pVertex++ = vNormal.z;

      *pVertex++ = (float) seg / (float) SPHERE_SEGS;
      *pVertex++ = (float) ring / (float) SPHERE_RINGS;

      if (ring != SPHERE_RINGS) {
        // each vertex (except the last) has six indices pointing to it
        *pIndices++ = wVerticeIndex + SPHERE_SEGS + 1;
        *pIndices++ = wVerticeIndex;               
        *pIndices++ = wVerticeIndex + SPHERE_SEGS;
        *pIndices++ = wVerticeIndex + SPHERE_SEGS + 1;
        *pIndices++ = wVerticeIndex + 1;
        *pIndices++ = wVerticeIndex;
        wVerticeIndex ++;
      }
    }; // end for seg
  } // end for ring

  // Unlock
  vBuf->unlock();
  iBuf->unlock();
  // Generate face list
  pSphereVertex->useSharedVertices = true;

  // the original code was missing this line:
  pSphere->_setBounds(
    AxisAlignedBox( 
      Vector3(-SPHERE_RADIUS, -SPHERE_RADIUS, -SPHERE_RADIUS),
      Vector3(SPHERE_RADIUS, SPHERE_RADIUS, SPHERE_RADIUS)),
    false);
  pSphere->_setBoundingSphereRadius(SPHERE_RADIUS);
  // this line makes clear the mesh is loaded (avoids memory leaks)
  pSphere->load();
}

BaseSphere::BaseSphere() : mpNode(nullptr), mGraphPos(0, 0),
  isChecked(false), mpEnt(nullptr), mType(GRAY)
{
  for (BYTE i= 0; i < 4; ++i)
    mpAttachPoints[i]= nullptr;
}

BaseSphere::~BaseSphere()
{

}

const Vector3 BaseSphere::LEFT_POINT  = Vector3(-DIAMETER, 0, 0);
const Vector3 BaseSphere::RIGHT_POINT = Vector3( DIAMETER, 0, 0);
const Vector3 BaseSphere::BOTTOM_POINT= Vector3(0, -DIAMETER, 0);
const Vector3 BaseSphere::TOP_POINT   = Vector3(0,  DIAMETER, 0);

bool BaseSphere::IsIntersection(BaseSphere* pSphere)
{
  return (pSphere->GetDerivedPosition() - mpNode->_getDerivedPosition()).squaredLength() 
    <= 4 * (SPHERE_RADIUS * SPHERE_RADIUS);
}

BYTE BaseSphere::ClosestAttachPoint(BaseSphere* pBall)
{
  Vector3 p1= pBall->GetDerivedPosition();

  Real distances[4];
  for (BYTE i= 0; i < 4; ++i)
    distances[i]= mpAttachPoints[i]->_getDerivedPosition().squaredDistance(p1);

  BYTE numPoint= min_element(distances, distances + 4) - distances;

  if (numPoint >= 0 && numPoint < 4)
    return numPoint;
  else
    throw runtime_error("Error while calculating distance to an attach point.");

  return 0xff;
}

SceneNode* BaseSphere::GetNode()
{
  return mpNode;
}

BaseSphere::sphere_type BaseSphere::GetType()
{
  return mType;
}

const Ogre::Vector3& BaseSphere::GetPosition()
{
  return mpNode->getPosition();
}

const Ogre::Vector3& BaseSphere::GetDerivedPosition()
{
  return mpNode->_getDerivedPosition();
}

BaseSphere::direction BaseSphere::Mirror(const direction DIRECTION)
{
  // Returns the opposite direction
  return (DIRECTION == LEFT ? RIGHT : (DIRECTION == TOP ? BOTTOM : (DIRECTION == RIGHT ? LEFT : TOP)));
}

pair<BYTE, BYTE> BaseSphere::MirrorPos(BaseSphere* pSphere, const direction DIRECTION)
{
  auto pos= pSphere->GetGraphPos();

  switch (DIRECTION)
  {
  case LEFT:
    return make_pair(pos.first - 1, pos.second);
    break;
  case RIGHT:
    return make_pair(pos.first + 1, pos.second);
    break;
  case TOP:
    return make_pair(pos.first, pos.second - 1);
    break;
  case BOTTOM:
    return make_pair(pos.first, pos.second + 1);
    break;
  default:
    throw logic_error("An invalid direction was supplied to TripleGasm::BaseSphere::Mirror");
  }

  return make_pair(0xff, 0xff);
}

const pair<BYTE, BYTE>& BaseSphere::GetGraphPos()
{
  return mGraphPos;
}

SceneManager* BaseSphere::GetSceneManager()
{
  return mpEnt->_getManager();
}

const Ogre::String& BaseSphere::GetName()
{
  return mpNode->getName();
}

void BaseSphere::Destroy()
{
  for (BYTE i= 0; i < 4; ++i)
  {
    auto p= mpAttachPoints[i];

    if (GetAttachPointData(p) == nullptr)
    {
      if (p->getName() != "")
      {
        p->getParent()->removeChild(p);
        GetSceneManager()->destroySceneNode(p);
      }
      mpAttachPoints[i]= nullptr;
    }
  }

  // Do not uncomment the following line:
  /*mpNode->getUserObjectBindings().setUserAny(static_cast<Any>(static_cast<BaseSphere*>(nullptr)));*/
  // The nodes data should be reset after all spheres are destroyed.
  mpNode->detachObject(mpEnt);
  GetSceneManager()->destroyEntity(mpEnt);
  // Do not uncomment this either, reason above ^
  // mpNode= nullptr;
  mpEnt = nullptr;
}

BaseSphere* BaseSphere::GetAttachPointData(const direction NUM_POINT)
{
  auto data= mpAttachPoints[NUM_POINT]->getUserObjectBindings().getUserAny();
  return data.isEmpty() ? nullptr : any_cast<BaseSphere*>(data);
}

void BaseSphere::BindAttachPoints(const direction NUM_POINT, BaseSphere* pSphere)
{
  // There is no attach point, so inherit the other spheres node.
  if (mpAttachPoints[NUM_POINT] == nullptr)
    mpAttachPoints[NUM_POINT]= pSphere->GetNode();

  // The opposite spheres attach points should also be correct.
  pSphere->mpAttachPoints[Mirror(NUM_POINT)]= mpNode;
}

void BaseSphere::SetAttachPoint(const direction NUM_POINT, SceneNode* pNode)
{
  if (mpAttachPoints[NUM_POINT] == nullptr)
  {
    // A new node was created, it is not attached to anything, so the 
    // data is a nullptr.
    pNode->getUserObjectBindings().setUserAny(
      static_cast<Any>(static_cast<BaseSphere*>(nullptr)));
  }
  mpAttachPoints[NUM_POINT]= pNode;
}

void BaseSphere::SetNodeData(BaseSphere* pSphere)
{
  mpNode->getUserObjectBindings().setUserAny(static_cast<Any>(pSphere));
}

BaseSphere* BaseSphere::GetAttachPointData(SceneNode* pNode)
{
  auto data= pNode->getUserObjectBindings().getUserAny();
  return data.isEmpty() ? nullptr : any_cast<BaseSphere*>(data);
}